package se.no.noko.engine.core;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;

import se.no.noko.engine.R;
import se.no.noko.engine.primitives.Cube;
import se.no.noko.engine.utils.FileLoader;
import android.opengl.GLES20;
import android.opengl.GLSurfaceView;
import android.opengl.Matrix;
import android.util.Log;

/**
 * This class implements our custom renderer. Note that the GL10 parameter
 * passed in is unused for OpenGL ES 2.0 renderers -- the static class GLES20 is
 * used instead.
 */
public class Scene implements GLSurfaceView.Renderer {

	private static final String TAG = "Noko-3D-Engine";
	private final Matrices matrices = new Matrices();
	private final LightCoordinates lightCoordinates = new LightCoordinates();
	private final HashMap<String, Integer> handles = new HashMap<String, Integer>();
	private final List<Renderable> renderables = new ArrayList<Renderable>();
	private int mPointProgramHandle;
	private int mPerVertexProgramHandle;

	/**
	 * Initialize the model data.
	 */
	public Scene() {
		// // This triangle is red, green, and blue.
		// Triangle triangle1 = new Triangle(new float[] { -0.5f, -0.25f, 0.0f,
		// 1.0f, 0.0f, 0.0f, 1.0f, 0.5f, -0.25f, 0.0f, 0.0f, 0.0f, 1.0f,
		// 1.0f, 0.0f, 0.559016994f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f });
		// renderables.add(triangle1);
		//
		// // This triangle is yellow, cyan, and magenta.
		// Triangle triangle2 = new Triangle(new float[] { -0.5f, -0.25f, 0.0f,
		// 1.0f, 1.0f, 0.0f, 1.0f, 0.5f, -0.25f, 0.0f, 0.0f, 1.0f, 1.0f,
		// 1.0f, 0.0f, 0.559016994f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f });
		// renderables.add(triangle2);
		//
		// // This triangle is white, gray, and black.
		// Triangle triangle3 = new Triangle(new float[] { -0.5f, -0.25f, 0.0f,
		// 1.0f, 1.0f, 1.0f, 1.0f, 0.5f, -0.25f, 0.0f, 0.5f, 0.5f, 0.5f,
		// 1.0f, 0.0f, 0.559016994f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f });
		// renderables.add(triangle3);
		//
		// Rectangle rectangle1 = new Rectangle(new float[] { -1f, -1f, -3f,
		// 1.0f,
		// 1.0f, 0.0f, 1.0f, 1f, -1f, -3f, 0.0f, 1.0f, 1.0f, -1.0f, -1f,
		// 1.0f, -3f, 1.0f, 0.0f, 1.0f, 1.0f, 1f, 1f, -3f, 1.0f, 1.0f,
		// 0.0f, 1.0f });
		//
		// renderables.add(rectangle1);
		Cube cube = new Cube();
		renderables.add(cube);
	}

	@Override
	public void onSurfaceChanged(GL10 glUnused, int width, int height) {
		// Set the OpenGL viewport to the same size as the surface.
		GLES20.glViewport(0, 0, width, height);

		// Create a new perspective projection matrix. The height will stay the
		// same
		// while the width will vary as per aspect ratio.
		final float ratio = (float) width / height;
		final float left = -ratio;
		final float right = ratio;
		final float bottom = -1.0f;
		final float top = 1.0f;
		final float near = 1.0f;
		final float far = 10.0f;

		Matrix.frustumM(matrices.getProjectionMatrix(), 0, left, right, bottom,
				top, near, far);
	}

	@Override
	public void onDrawFrame(GL10 glUnused) {
		// GLES20.glClear(GLES20.GL_DEPTH_BUFFER_BIT |
		// GLES20.GL_COLOR_BUFFER_BIT);
		//
		// // Do a complete rotation every 10 seconds.
		// long time = SystemClock.uptimeMillis() % 10000L;
		// float angleInDegrees = (360.0f / 10000.0f) * ((int) time);

		// // Set our per-vertex lighting program.
		// GLES20.glUseProgram(mPerVertexProgramHandle);
		//
		// // Set program handles for cube drawing.
		// handles.put(Handles.MVP_MATRIX_HANDLE.toString(), GLES20
		// .glGetUniformLocation(mPerVertexProgramHandle, "u_MVPMatrix"));
		// handles.put(Handles.MV_MATRIX_HANDLE.toString(), GLES20
		// .glGetUniformLocation(mPerVertexProgramHandle, "u_MVMatrix"));
		// handles.put(Handles.LIGHT_POS_HANDLE.toString(), GLES20
		// .glGetUniformLocation(mPerVertexProgramHandle, "u_LightPos"));
		// handles.put(Handles.POSITION_HANDLE.toString(), GLES20
		// .glGetAttribLocation(mPerVertexProgramHandle, "a_Position"));
		// handles.put(Handles.COLOR_HANDLE.toString(),
		// GLES20.glGetAttribLocation(mPerVertexProgramHandle, "a_Color"));
		// handles.put(Handles.NORMAL_HANDLE.toString(),
		// GLES20.glGetAttribLocation(mPerVertexProgramHandle, "a_Normal"));
		//
		// // Calculate position of the light. Rotate and then push into the
		// // distance.
		// Matrix.setIdentityM(matrices.getLightModelMatrix(), 0);
		// Matrix.translateM(matrices.getLightModelMatrix(), 0, 0.0f, 0.0f,
		// -5.0f);
		// Matrix.rotateM(matrices.getLightModelMatrix(), 0, angleInDegrees,
		// 0.0f,
		// 1.0f, 0.0f);
		// Matrix.translateM(matrices.getLightModelMatrix(), 0, 0.0f, 0.0f,
		// 2.0f);
		//
		// Matrix.multiplyMV(lightCoordinates.getLightPosInWorldSpace(), 0,
		// matrices.getLightModelMatrix(), 0,
		// lightCoordinates.getLightPosInModelSpace(), 0);
		// Matrix.multiplyMV(lightCoordinates.getLightPosInEyeSpace(), 0,
		// matrices.getViewMatrix(), 0,
		// lightCoordinates.getLightPosInWorldSpace(), 0);
		//
		// // Draw some cubes.
		// Matrix.setIdentityM(matrices.getModelMatrix(), 0);
		// Matrix.translateM(matrices.getModelMatrix(), 0, 4.0f, 0.0f, -7.0f);
		// Matrix.rotateM(matrices.getModelMatrix(), 0, angleInDegrees, 1.0f,
		// 0.0f, 0.0f);
		// renderables.get(0).render(matrices, handles, lightCoordinates);
		//
		// Matrix.setIdentityM(matrices.getModelMatrix(), 0);
		// Matrix.translateM(matrices.getModelMatrix(), 0, -4.0f, 0.0f, -7.0f);
		// Matrix.rotateM(matrices.getModelMatrix(), 0, angleInDegrees, 0.0f,
		// 1.0f, 0.0f);
		// renderables.get(0).render(matrices, handles, lightCoordinates);
		//
		// Matrix.setIdentityM(matrices.getModelMatrix(), 0);
		// Matrix.translateM(matrices.getModelMatrix(), 0, 0.0f, 4.0f, -7.0f);
		// Matrix.rotateM(matrices.getModelMatrix(), 0, angleInDegrees, 0.0f,
		// 0.0f, 1.0f);
		// renderables.get(0).render(matrices, handles, lightCoordinates);
		//
		// Matrix.setIdentityM(matrices.getModelMatrix(), 0);
		// Matrix.translateM(matrices.getModelMatrix(), 0, 0.0f, -4.0f, -7.0f);
		// renderables.get(0).render(matrices, handles, lightCoordinates);
		//
		// Matrix.setIdentityM(matrices.getModelMatrix(), 0);
		// Matrix.translateM(matrices.getModelMatrix(), 0, 0.0f, 0.0f, -5.0f);
		// Matrix.rotateM(matrices.getModelMatrix(), 0, angleInDegrees, 1.0f,
		// 1.0f, 0.0f);
		// renderables.get(0).render(matrices, handles, lightCoordinates);

		// Draw a point to indicate the light.
		GLES20.glUseProgram(mPointProgramHandle);
		drawLight();
	}

	/**
	 * Helper function to compile a shader.
	 * 
	 * @param shaderType
	 *            The shader type.
	 * @param shaderSource
	 *            The shader source code.
	 * @return An OpenGL handle to the shader.
	 */
	private int compileShader(final int shaderType, final String shaderSource) {
		int shaderHandle = GLES20.glCreateShader(shaderType);

		if (shaderHandle != 0) {
			// Pass in the shader source.
			GLES20.glShaderSource(shaderHandle, shaderSource);

			// Compile the shader.
			GLES20.glCompileShader(shaderHandle);

			// Get the compilation status.
			final int[] compileStatus = new int[1];
			GLES20.glGetShaderiv(shaderHandle, GLES20.GL_COMPILE_STATUS,
					compileStatus, 0);

			// If the compilation failed, delete the shader.
			if (compileStatus[0] == 0) {
				Log.e(TAG,
						"Error compiling shader: "
								+ GLES20.glGetShaderInfoLog(shaderHandle));
				GLES20.glDeleteShader(shaderHandle);
				shaderHandle = 0;
			}
		}

		if (shaderHandle == 0) {
			throw new RuntimeException("Error creating shader.");
		}

		return shaderHandle;
	}

	/**
	 * Helper function to compile and link a program.
	 * 
	 * @param vertexShaderHandle
	 *            An OpenGL handle to an already-compiled vertex shader.
	 * @param fragmentShaderHandle
	 *            An OpenGL handle to an already-compiled fragment shader.
	 * @param attributes
	 *            Attributes that need to be bound to the program.
	 * @return An OpenGL handle to the program.
	 */
	private int createAndLinkProgram(final int vertexShaderHandle,
			final int fragmentShaderHandle, final String[] attributes) {
		int programHandle = GLES20.glCreateProgram();

		if (programHandle != 0) {
			// Bind the vertex shader to the program.
			GLES20.glAttachShader(programHandle, vertexShaderHandle);

			// Bind the fragment shader to the program.
			GLES20.glAttachShader(programHandle, fragmentShaderHandle);

			// Bind attributes
			if (attributes != null) {
				final int size = attributes.length;
				for (int i = 0; i < size; i++) {
					GLES20.glBindAttribLocation(programHandle, i, attributes[i]);
				}
			}

			// Link the two shaders together into a program.
			GLES20.glLinkProgram(programHandle);

			// Get the link status.
			final int[] linkStatus = new int[1];
			GLES20.glGetProgramiv(programHandle, GLES20.GL_LINK_STATUS,
					linkStatus, 0);

			// If the link failed, delete the program.
			if (linkStatus[0] == 0) {
				Log.e(TAG,
						"Error compiling program: "
								+ GLES20.glGetProgramInfoLog(programHandle));
				GLES20.glDeleteProgram(programHandle);
				programHandle = 0;
			}
		}

		if (programHandle == 0) {
			throw new RuntimeException("Error creating program.");
		}

		return programHandle;
	}

	/**
	 * Draws a point representing the position of the light.
	 */
	private void drawLight() {
		final int pointMVPMatrixHandle = GLES20.glGetUniformLocation(
				mPointProgramHandle, "u_MVPMatrix");
		final int pointPositionHandle = GLES20.glGetAttribLocation(
				mPointProgramHandle, "a_Position");

		// Pass in the position.
		GLES20.glVertexAttrib3f(pointPositionHandle,
				lightCoordinates.getLightPosInModelSpace()[0],
				lightCoordinates.getLightPosInModelSpace()[1],
				lightCoordinates.getLightPosInModelSpace()[2]);

		// Since we are not using a buffer object, disable vertex arrays for
		// this attribute.
		GLES20.glDisableVertexAttribArray(pointPositionHandle);

		// Pass in the transformation matrix.
		Matrix.multiplyMM(matrices.getMvpMatrix(), 0, matrices.getViewMatrix(),
				0, matrices.getLightModelMatrix(), 0);
		Matrix.multiplyMM(matrices.getMvpMatrix(), 0,
				matrices.getModelMatrix(), 0, matrices.getMvpMatrix(), 0);
		GLES20.glUniformMatrix4fv(pointMVPMatrixHandle, 1, false,
				matrices.getMvpMatrix(), 0);

		// Draw the point.
		GLES20.glDrawArrays(GLES20.GL_POINTS, 0, 1);
	}

	@Override
	public void onSurfaceCreated(GL10 arg0, EGLConfig arg1) {
		// Set the background clear color to black.
		GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);

		// Use culling to remove back faces.
		GLES20.glEnable(GLES20.GL_CULL_FACE);

		// Enable depth testing
		GLES20.glEnable(GLES20.GL_DEPTH_TEST);

		// Position the eye in front of the origin.
		final float eyeX = 0.0f;
		final float eyeY = 0.0f;
		final float eyeZ = -0.5f;

		// We are looking toward the distance
		final float lookX = 0.0f;
		final float lookY = 0.0f;
		final float lookZ = -5.0f;

		// Set our up vector. This is where our head would be pointing were we
		// holding the camera.
		final float upX = 0.0f;
		final float upY = 1.0f;
		final float upZ = 0.0f;

		// Set the view matrix. This matrix can be said to represent the camera
		// position.
		// NOTE: In OpenGL 1, a ModelView matrix is used, which is a combination
		// of a model and
		// view matrix. In OpenGL 2, we can keep track of these matrices
		// separately if we choose.
		Matrix.setLookAtM(matrices.getViewMatrix(), 0, eyeX, eyeY, eyeZ, lookX,
				lookY, lookZ, upX, upY, upZ);

		final String vertexShader = "uniform mat4 u_MVPMatrix;      \n" // A
																		// constant
																		// representing
																		// the
																		// combined
																		// model/view/projection
																		// matrix.
				+ "uniform mat4 u_MVMatrix;       \n" // A constant representing
														// the combined
														// model/view matrix.
				+ "uniform vec3 u_LightPos;       \n" // The position of the
														// light in eye space.

				+ "attribute vec4 a_Position;     \n" // Per-vertex position
														// information we will
														// pass in.
				+ "attribute vec4 a_Color;        \n" // Per-vertex color
														// information we will
														// pass in.
				+ "attribute vec3 a_Normal;       \n" // Per-vertex normal
														// information we will
														// pass in.

				+ "varying vec4 v_Color;          \n" // This will be passed
														// into the fragment
														// shader.

				+ "void main()                    \n" // The entry point for our
														// vertex shader.
				+ "{                              \n"
				// Transform the vertex into eye space.
				+ "   vec3 modelViewVertex = vec3(u_MVMatrix * a_Position);              \n"
				// Transform the normal's orientation into eye space.
				+ "   vec3 modelViewNormal = vec3(u_MVMatrix * vec4(a_Normal, 0.0));     \n"
				// Will be used for attenuation.
				+ "   float distance = length(u_LightPos - modelViewVertex);             \n"
				// Get a lighting direction vector from the light to the vertex.
				+ "   vec3 lightVector = normalize(u_LightPos - modelViewVertex);        \n"
				// Calculate the dot product of the light vector and vertex
				// normal. If the normal and light vector are
				// pointing in the same direction then it will get max
				// illumination.
				+ "   float diffuse = max(dot(modelViewNormal, lightVector), 0.1);       \n"
				// Attenuate the light based on distance.
				+ "   diffuse = diffuse * (1.0 / (1.0 + (0.25 * distance * distance)));  \n"
				// Multiply the color by the illumination level. It will be
				// interpolated across the triangle.
				+ "   v_Color = a_Color * diffuse;                                       \n"
				// gl_Position is a special variable used to store the final
				// position.
				// Multiply the vertex by the matrix to get the final point in
				// normalized screen coordinates.
				+ "   gl_Position = u_MVPMatrix * a_Position;                            \n"
				+ "}                                                                     \n";

		final String fragmentShader = "precision mediump float;       \n" // Set
																			// the
																			// default
																			// precision
																			// to
																			// medium.
																			// We
																			// don't
																			// need
																			// as
																			// high
																			// of
																			// a
																			// precision
																			// in
																			// the
																			// fragment
																			// shader.
				+ "varying vec4 v_Color;          \n" // This is the color from
														// the vertex shader
														// interpolated across
														// the
														// triangle per
														// fragment.
				+ "void main()                    \n" // The entry point for our
														// fragment shader.
				+ "{                              \n"
				+ "   gl_FragColor = v_Color;     \n" // Pass the color directly
														// through the pipeline.
				+ "}                              \n";

		final int vertexShaderHandle = compileShader(GLES20.GL_VERTEX_SHADER,
				vertexShader);
		final int fragmentShaderHandle = compileShader(
				GLES20.GL_FRAGMENT_SHADER, fragmentShader);

		mPerVertexProgramHandle = createAndLinkProgram(vertexShaderHandle,
				fragmentShaderHandle, new String[] { "a_Position", "a_Color",
						"a_Normal" });

		// Define a simple shader program for our point.
		final String pointVertexShader = getLightPosVertexShader();

		final String pointFragmentShader = getLightPosFragmentShader();

		final int pointVertexShaderHandle = compileShader(
				GLES20.GL_VERTEX_SHADER, pointVertexShader);
		final int pointFragmentShaderHandle = compileShader(
				GLES20.GL_FRAGMENT_SHADER, pointFragmentShader);
		mPointProgramHandle = createAndLinkProgram(pointVertexShaderHandle,
				pointFragmentShaderHandle, new String[] { "a_Position" });

	}

	private String getLightPosFragmentShader() {
		return FileLoader.loadPlainText(R.raw.light_pos_fragment_shader);
	}

	private String getLightPosVertexShader() {
		return FileLoader.loadPlainText(R.raw.light_pos_vertex_shader);
	}

	private String getFragmentShader() {
		return FileLoader.loadPlainText(R.raw.fragment_shader);
	}

	private String getVertexShader() {
		return FileLoader.loadPlainText(R.raw.light_pos_vertex_shader);
	}
}
